理解 CSS 画三角形的原理,了解 em 单位的最佳实践,并且使用 React 搭配 classnames 封装原子组件
CSS 画三角形
- 三角形为等腰直角三角形
 - 主要利用的核心属性就是 
border 
首先,我们先画一个普通的正方形:
div {
  width: 100px;
  height: 100px;
  border: 1px solid skyblue;
}
下面我们将边框的宽度数值调为调整为 width 的一半:
div {
  width: 100px;
  height: 100px;
  border: solid skyblue;
  border-width: 50px;
}

然后给四个边设置为不同的颜色:
div {
  width: 100px;
  height: 100px;
  border-style: solid;
  border-width: 50px;
  border-color: orange gold skyblue pink;
}

接着设置为 border-box:
div {
  width: 100px;
  height: 100px;
  border-style: solid;
  border-width: 50px;
  border-color: orange gold skyblue pink;
  box-sizing: border-box;
}
border-box 会让盒子的 width/height 属性控制 border + padding + content 的大小,
由于 margin 为 0,因此目前整个盒子的占位大小为 100px * 100px,全部都由 border 来占位:

可以看见,整个盒子都是由四个边框构成的,每个边框都是一个 等腰直角三角形, 接下来想办法切割想要的方向的三角形即可。
这里我们来切割蓝色的那个三角形,只需要把其他三个边框设置为透明色:
div {
  width: 100px;
  height: 100px;
  border-style: solid;
  border-width: 50px;
  border-color: transparent transparent skyblue transparent;
  box-sizing: border-box;
}
这样我们就得到了蓝色的三角形:

但是它的占位有点问题:

想要把它的高度也调整为 50px,就需要把对边宽度调整为 0,并且把指定的 width/height 去掉:
div {
  border-style: solid;
  border-width: 0 50px 50px 50px;
  border-color: transparent transparent skyblue transparent;
  box-sizing: border-box;
}

这样一来我们就得到了一个完美的 等腰直角三角形。这个三角形的底部等于 border-width * 2,高等于 border-width。
下面来总结一下核心要点:
- 设置怪异盒子模型:
box-sizing: border-box - 边框设置为实线:
border-style: solid - 设置边框宽度即三角形的高度,想要的三角形对边宽度设置为 0:
border-width: 0 50px 50px 50px - 留下想要的三角形的颜色,其余边框设置为透明色:
border-color: transparent transparent skyblue transparent 
改造三角形
由 font-size 决定大小
在上面我们所画的等腰直角三角形中,它的高度是固定的,现在我想把它改造为一个字体图标,通过 font-size 就能够改变它的大小。
这时需要把 div 替换为一个 span,然后在外层包裹一个 div:
<div class='icon-triangle'>
  <span></span>
</div>
.icon-triangle {
  display: inline-block;
  line-height: 0;
  span {
    display: inline-block;
    border-style: solid;
    border-width: 0 1em 1em 1em;
    border-color: transparent transparent skyblue transparent;
    box-sizing: border-box;
  }
}
需要注意的是这里要给外层 div 设置为内联块,并且添加一个 line-height: 0,然后还要给里面的 span 设置为内联块,目的是让外层 div 宽高完全由内部的 span 高度撑开。
最重要的就是把内部 span 的 border-width 的单位设置为 em,作用是让等腰直角三角形的高由 .icon-triangle 的 font-size 决定。
这样一来,.icon-triangle 的 font-size 就是等腰三角形的高度了,比如 font-size: 30px,那么这个等腰三角形的高度就是 30px。
在直角顶添加一条直线
对于一些视频播放的继续播放按钮,通常是下面这个样子:
下面我们继续改造我们的三角形,为直角顶添加一条直线:
.icon-triangle {
  display: inline-block;
  line-height: 0;
  span {
    position: relative;
    display: inline-block;
    box-sizing: border-box;
    border-style: solid;
    border-width: 1em 1em 0em 1em;
    border-color: orange transparent transparent transparent;
    &::after {
      content: "";
      position: absolute;
      top: 0.2em;
      left: -1em;
      width: 2em;
      border-top: 0.2em solid;
      border-top-color: inherit;
    }
  }
}
在 span 后面加上一个虚元素,然后通过绝对定位,向下移动一段距离即可:
为了让它由 font-size 决定大小,因此使用的 em 单位,唯一的瑕疵就是这个伪类是脱离了文档流的,整个图标盒子大小没有包含这个伪类进去:

最后,想要调整这个图标的方向,只需要使用 transform: rotate 来旋转指定的角度即可。
封装 TriangleButton 组件
这里我们使用了 classnames 这个库,它的主要作用就是用来根据传入 React 组件的 props 进行类名拼接从而实现不同的样式。
Props 接口定义
首先,我们给 TriangleButton 定义一个 Props 接口,它基本上抽象出了整个组件的功能:
interface TriangleButtonProps {
  // 图标大小
  size?: "small" | "normal" | "large" | number;
  // 图标颜色
  color?: "black" | "white" | "orange" | "skyblue" | "red";
  // 是否有垂直线
  hasVerticalLine?: boolean;
  // 图标方向
  direction?: "up" | "right" | "down" | "left";
  // 点击事件回调
  onClick?: React.MouseEventHandler;
}
组件实现
const TriangleButton: React.FC<TriangleButtonProps> = (props) => {
  const { size, color, direction, hasVerticalLine, onClick } = props;
  const classes = classnames("triangle-button", {
    left: direction === "left",
    right: direction === "right",
    down: direction === "down",
    up: direction === "up",
    "small-size": size === "small",
    "normal-size": size === "normal",
    "large-size": size === "large",
    "vertical-line": hasVerticalLine,
  });
  const sizeStyle = typeof size === "number" ? { fontSize: size + "px" } : {};
  const colorStyle = ["black", "white", "orange", "skyblue", "red"].includes(
    color
  )
    ? { borderTopColor: color }
    : {};
  const style = { ...sizeStyle, ...colorStyle };
  return (
    <div className={classes} onClick={(e) => onClick?.(e)}>
      <span style={style}></span>
    </div>
  );
};
TriangleButton.defaultProps = {
  color: "black",
  size: "normal",
  direction: "left",
  hasVerticalLine: false,
};
export default TriangleButton;
样式实现
$direction-map: (
  "top": rotate(180deg),
  "right": rotate(270deg),
  "down": rotate(0deg),
  "left": rotate(90deg),
);
$size-map: (
  "small-size": 8px,
  "normal-size": 16px,
  "large-size": 24px,
);
.triangle-button {
  display: inline-block;
  line-height: 0;
  cursor: pointer;
  @each $key, $value in $direction-map {
    &.#{$key} {
      transform: $value;
    }
  }
  @each $key, $value in $size-map {
    &.#{$key} {
      font-size: $value;
    }
  }
  &.vertical-line {
    span::after {
      content: "";
      position: absolute;
      top: 0.2em;
      left: -1em;
      width: 2em;
      border-top: 0.2em solid;
      border-top-color: inherit;
    }
  }
  span {
    display: inline-block;
    border-style: solid;
    border-width: 1em 1em 0em 1em;
    border-color: black transparent transparent transparent;
    box-sizing: border-box;
    position: relative;
  }
}